﻿//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Microsoft Public License.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace AstoriaOverAstoria
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;

    /// <summary>Helper methods for working with types and reflection.</summary>
    internal static class TypeSystem
    {
        /// <summary>Enumerable.Select method info</summary>
        private static readonly MethodInfo SelectMethodInfoEnumerable = typeof(Enumerable)
                                                        .GetMethods(BindingFlags.Static | BindingFlags.Public)
                                                        .Single(IsMethodLinqSelect);

        /// <summary>Queryable.Select method info</summary>
        private static readonly MethodInfo SelectMethodInfoQueryable = typeof(Queryable)
                                                        .GetMethods(BindingFlags.Static | BindingFlags.Public)
                                                        .Single(IsMethodLinqSelect);

        /// <summary>Array of projected propery names used for fast lookup based on the projected property index.</summary>
        private static string[] projectedPropertyNames = new string[]
        {
            "ProjectedProperty0",
            "ProjectedProperty1",
            "ProjectedProperty2",
            "ProjectedProperty3",
            "ProjectedProperty4",
            "ProjectedProperty5",
            "ProjectedProperty6",
            "ProjectedProperty7",
            "ProjectedProperty8",
            "ProjectedProperty9",
            "ProjectedProperty10",
            "ProjectedProperty11"
        };

        /// <summary>
        /// Gets the elementtype for enumerable
        /// </summary>
        /// <param name="type">The type to inspect.</param>
        /// <returns>If the <paramref name="type"/> was IEnumerable then it returns the type of a single element
        /// otherwise it returns null.</returns>
        public static Type GetIEnumerableElementType(Type type)
        {
            Type ienum = FindIEnumerable(type);
            if (ienum == null)
            {
                return null;
            }

            return ienum.GetGenericArguments()[0];
        }

        /// <summary>
        /// Finds type that implements IEnumerable so can get elemtent type
        /// </summary>
        /// <param name="seqType">The Type to check</param>
        /// <returns>returns the type which implements IEnumerable</returns>
        public static Type FindIEnumerable(Type seqType)
        {
            if (seqType == null || seqType == typeof(string))
            {
                return null;
            }

            if (seqType.IsArray)
            {
                return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
            }

            if (seqType.IsGenericType)
            {
                foreach (Type arg in seqType.GetGenericArguments())
                {
                    Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);
                    if (ienum.IsAssignableFrom(seqType))
                    {
                        return ienum;
                    }
                }
            }

            Type[] ifaces = seqType.GetInterfaces();
            if (ifaces != null && ifaces.Length > 0)
            {
                foreach (Type iface in ifaces)
                {
                    Type ienum = FindIEnumerable(iface);
                    if (ienum != null)
                    {
                        return ienum;
                    }
                }
            }

            if (seqType.BaseType != null && seqType.BaseType != typeof(object))
            {
                return FindIEnumerable(seqType.BaseType);
            }

            return null;
        }

        /// <summary>
        /// Finds type that implements IQueryable so can get elemtent type
        /// </summary>
        /// <param name="seqType">The Type to check</param>
        /// <returns>returns the type which implements IQueryable</returns>
        public static Type FindIQueryable(Type seqType)
        {
            if (seqType == null || seqType == typeof(string))
            {
                return null;
            }

            if (seqType.IsGenericType)
            {
                foreach (Type arg in seqType.GetGenericArguments())
                {
                    Type ienum = typeof(IQueryable<>).MakeGenericType(arg);
                    if (ienum.IsAssignableFrom(seqType))
                    {
                        return ienum;
                    }
                }
            }

            Type[] ifaces = seqType.GetInterfaces();
            if (ifaces != null && ifaces.Length > 0)
            {
                foreach (Type iface in ifaces)
                {
                    Type ienum = FindIQueryable(iface);
                    if (ienum != null)
                    {
                        return ienum;
                    }
                }
            }

            if (seqType.BaseType != null && seqType.BaseType != typeof(object))
            {
                return FindIQueryable(seqType.BaseType);
            }

            return null;
        }

        /// <summary>Determines if the <pararef name="type"/> is a ProjectedWrapper type.</summary>
        /// <param name="type">Type to inspect.</param>
        /// <returns>True if the <paramref name="type"/> is a ProjectedWrapper type.</returns>
        public static bool IsProjectedWrapperType(Type type)
        {
            return typeof(System.Data.Services.Internal.ProjectedWrapper).IsAssignableFrom(type);
        }

        /// <summary>Returns true if the specified type is one of the ExpandedWrapper types.</summary>
        /// <param name="type">Type to inspect.</param>
        /// <returns>True if the <paramref name="type"/> is an ExpandedWrapper type.</returns>
        public static bool IsExpandedWrapperType(Type type)
        {
            // Each ExpandedWrapper<T,T> derives from ExpandedWrapper<T>
            type = type.BaseType;
            if (type == null || !type.IsGenericType)
            {
                return false;
            }

            if (type.GetGenericTypeDefinition() == typeof(System.Data.Services.Internal.ExpandedWrapper<>))
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>Determines if the specified <paramref name="type"/> is nullable and if so returns the underlying non-nullable type.</summary>
        /// <param name="type">The type to inspect.</param>
        /// <returns>null if the specified <paramref name="type"/> is not nullable, otherwise it returns the underlying non-nullable type.</returns>
        public static Type GetNullableUnderlyingType(Type type)
        {
            if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(Nullable<>))
            {
                return null;
            }

            return type.GetGenericArguments()[0];
        }

        /// <summary>Checks whether the specified method a method on IEnumerable with Func`T,T2 parameter.</summary>
        /// <param name="m">Method to check.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        public static bool IsLinqNamedMethodSecondArgumentFunctionWithOneParameter(MethodInfo m, string methodName)
        {
            if (m.DeclaringType == typeof(Enumerable))
            {
                return IsNamedMethodSecondArgumentFuncWithOneParameter(m, methodName);
            }
            else
            {
                return IsNamedMethodSecondArgumentExpressionFuncWithOneParameter(m, methodName);
            }
        }

        /// <summary>Checks whether the specified method is the IEnumerable.Select() with Func`T,T2.</summary>
        /// <param name="m">Method to check.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        public static bool IsMethodLinqSelect(MethodInfo m)
        {
            return IsLinqNamedMethodSecondArgumentFunctionWithOneParameter(m, "Select");
        }

        /// <summary>Checks whether the specified method is the IEnumerable.SelectMany() with Func`T,T2.</summary>
        /// <param name="m">Method to check.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        public static bool IsMethodLinqSelectMany(MethodInfo m)
        {
            return IsLinqNamedMethodSecondArgumentFunctionWithOneParameter(m, "SelectMany");
        }

        /// <summary>Checks whether the specified method is the IEnumerable.Where() with Func`T,bool.</summary>
        /// <param name="m">Method to check.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        public static bool IsMethodLinqWhere(MethodInfo m)
        {
            return IsLinqNamedMethodSecondArgumentFunctionWithOneParameter(m, "Where");
        }

        /// <summary>Checks whether the specified method is the IEnumerable.OrderBy() with Func`T,T2.</summary>
        /// <param name="m">Method to check.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        public static bool IsMethodLinqOrderBy(MethodInfo m)
        {
            return IsLinqNamedMethodSecondArgumentFunctionWithOneParameter(m, "OrderBy");
        }

        /// <summary>Checks whether the specified method is the IEnumerable.ThenBy() with Func`T,T2.</summary>
        /// <param name="m">Method to check.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        public static bool IsMethodLinqThenBy(MethodInfo m)
        {
            return IsLinqNamedMethodSecondArgumentFunctionWithOneParameter(m, "ThenBy");
        }

        /// <summary>Returns the Select method to be called on the <paramref name="sourceType"/> with <paramref name="outputType"/>.</summary>
        /// <param name="sourceType">The type to call the Select on, this can be either IEnumerable'T or IQueryable'T</param>
        /// <param name="outputType">The return type of the Func the Select will be called with (the type of the results).</param>
        /// <returns>The Select method to use.</returns>
        public static MethodInfo GetLinqSelectMethod(Type sourceType, Type outputType)
        {
            Type queryable = FindIQueryable(sourceType);
            MethodInfo genericMethod;
            if (queryable != null)
            {
                genericMethod = SelectMethodInfoQueryable;
            }
            else
            {
                genericMethod = SelectMethodInfoEnumerable;
            }

            return genericMethod.MakeGenericMethod(GetIEnumerableElementType(sourceType), outputType);   
        }

        /// <summary>Creates a name of the ProjectedProperty for the specified <paramref name="projectedPropertyIndex"/>.</summary>
        /// <param name="projectedPropertyIndex">The index of the projected property.</param>
        /// <returns>The name of the projected property.</returns>
        /// <remarks>The naming scheme for projected properties is the same for both ProjectedWrapper and ExpandedWrapper.</remarks>
        public static string GetProjectedPropertyName(int projectedPropertyIndex)
        {
            Debug.Assert(projectedPropertyIndex >= 0 && projectedPropertyIndex < projectedPropertyNames.Length, "The projectedPropertyIndex is too small or too big.");
            return projectedPropertyNames[projectedPropertyIndex];
        }

        /// <summary>Determines the index of the projected property specified by its name.</summary>
        /// <param name="projectedPropertyName">The name of the projected property.</param>
        /// <returns>The index of the projected property.</returns>
        /// <remarks>The naming scheme for projected properties is the same for both ProjectedWrapper and ExpandedWrapper.</remarks>
        public static int GetProjectedPropertyIndex(string projectedPropertyName)
        {
            Debug.Assert(projectedPropertyName.StartsWith("ProjectedProperty"), "The specified name doesn't specified a ProjectedProperty.");
            return Int32.Parse(projectedPropertyName.Substring(17));
        }

        /// <summary>Checks whether the specified method takes a Expression`Func`T1,T2 as its second argument.</summary>
        /// <param name="m">Method to check.</param>
        /// <param name="name">Expected name of method.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        private static bool IsNamedMethodSecondArgumentExpressionFuncWithOneParameter(MethodInfo m, string name)
        {
            Debug.Assert(m != null, "m != null");
            Debug.Assert(!String.IsNullOrEmpty(name), "!String.IsNullOrEmpty(name)");
            if (m.Name == name)
            {
                ParameterInfo[] p = m.GetParameters();
                if (p != null &&
                    p.Length == 2 &&
                    p[0].ParameterType.IsGenericType &&
                    p[1].ParameterType.IsGenericType)
                {
                    Type expressionParameter = p[1].ParameterType;
                    Type[] genericArgs = expressionParameter.GetGenericArguments();
                    if (genericArgs.Length == 1 && expressionParameter.GetGenericTypeDefinition() == typeof(Expression<>))
                    {
                        Type functionParameter = genericArgs[0];
                        return functionParameter.IsGenericType && functionParameter.GetGenericTypeDefinition() == typeof(Func<,>);
                    }
                }
            }

            return false;
        }

        /// <summary>Checks whether the specified method takes a Func`T1,T2 as its second argument.</summary>
        /// <param name="m">Method to check.</param>
        /// <param name="name">Expected name of method.</param>
        /// <returns>true if this is the method; false otherwise.</returns>
        private static bool IsNamedMethodSecondArgumentFuncWithOneParameter(MethodInfo m, string name)
        {
            Debug.Assert(m != null, "m != null");
            Debug.Assert(!String.IsNullOrEmpty(name), "!String.IsNullOrEmpty(name)");
            if (m.Name == name)
            {
                ParameterInfo[] p = m.GetParameters();
                if (p != null &&
                    p.Length == 2 &&
                    p[0].ParameterType.IsGenericType &&
                    p[1].ParameterType.IsGenericType)
                {
                    Type functionParameter = p[1].ParameterType;
                    return functionParameter.IsGenericType && functionParameter.GetGenericTypeDefinition() == typeof(Func<,>);
                }
            }

            return false;
        }
    }
}